<?php
/**
 * Created by paperphp
 * User: 22071
 * Date: 2019/9/3
 * Email: <zhendongdong@foxmail.com>
 */

namespace paper\dao;

use paper\exception\DbException;
use PDO;
use PDOException;
use PDOStatement;

class Connection
{
    private PDO $pdo;
    private array $config;
    private bool $connected = false;

    public function __construct($config)
    {
        $this->config = $config;
        $this->connect();
    }

    public function connect()
    {
        if ($this->connected) {
            return;
        }
        $hostname = $this->config['hostname'] ?? 'localhost';
        $port = $this->config['port'] ?? 3306;
        $database = $this->config['database'] ?? 'database';
        $charset = $this->config['charset'] ?? 'utf8';
        $user = $this->config['user'] ?? '';
        $password = $this->config['password'] ?? '';

        $dsn = 'mysql:host=' . $hostname . ';port=' . $port . ';dbname=' .
            $database . ';charset=' . $charset;

        $this->pdo = new PDO($dsn, $user, $password);
        $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
        $this->errMode(PDO::ERRMODE_EXCEPTION);

        $this->connected = true;
    }

    /**
     * @return bool
     */
    public function isConnected(): bool
    {
        return $this->connected;
    }

    public function errMode(int $mode)
    {
        $this->pdo()->setAttribute(PDO::ATTR_ERRMODE, $mode);
    }


    /**
     * @param $key
     * @return mixed
     */
    public function getConfig($key): mixed
    {
        return $this->config[$key] ?? null;
    }

    /**
     * @return PDO|null
     */
    public function pdo(): ?PDO
    {
        return $this->pdo;
    }

    /**
     * @param $sql
     * @return PDOStatement
     * @throws DbException
     */
    public function prepare($sql): PDOStatement
    {
        try {
            $statement = $this->pdo()->prepare($sql);
            $statement->setFetchMode(PDO::FETCH_ASSOC);
            return $statement;
        } catch (PDOException $exception) {
            $dbException = new DbException($exception->getMessage());
            $dbException->setSql($sql);
            throw  $dbException;
        }
    }


    /**
     * 开启一个事务
     */
    public function startTrans()
    {
        $this->pdo->beginTransaction();
    }

    /**
     * 检查是否已经在事务中
     */
    public function inTransaction(): bool
    {
        return $this->pdo()->inTransaction();
    }

    /**
     * 回滚事务
     */
    public function rollback()
    {
        $this->pdo()->rollBack();
    }

    /**
     * 提交
     */
    public function commit()
    {
        $this->pdo()->commit();
    }


    /**
     * @param string|PDOStatement $sql
     * @param array $data
     * @return bool|int
     * @throws DbException
     */
    public function execute(string|PDOStatement $sql, array $data = []): bool|int
    {
        //实验特性，如果没有进行参数绑定那么直接查询而不是使用预查询
        if (!$data || count($data) == 0) {
            return $this->pdo->exec($sql);
        }

        if ($sql instanceof PDOStatement) {
            $statement = $sql;
        } else {
            $statement = $this->prepare($sql);
        }
        return $statement->execute($data) ? $statement->rowCount() : false;
    }

    public function quote(string $string): bool|string
    {
        return $this->pdo()->quote($string);
    }


    /**
     * @param string $sql
     * @param array $data
     * @return array|false
     * @throws DbException
     */
    public function fetchAll(string $sql, array $data = []): bool|array
    {
        if (!$statement = $this->getStatement($sql, $data))
            return false;
        return $statement->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * @param string $sql
     * @param array $data
     * @return bool|array
     * @throws DbException
     */
    public function fetch(string $sql, array $data = []): bool|array
    {
        if (!$statement = $this->getStatement($sql, $data))
            return false;
//        if ($objectClass)
//            return $statement->fetchObject($objectClass);
        return $statement->fetch(PDO::FETCH_ASSOC) ?: false;
    }

    /**
     * @param $sql
     * @param array $data
     * @return false|PDOStatement
     * @throws DbException
     */
    private function getStatement($sql, array $data = []): bool|PDOStatement
    {
        try {
            if ($sql instanceof PDOStatement) {
                $statement = $sql;
            } else {
                if (!$data || count($data) == 0) {
                    return $this->pdo()->query($sql);
                }
                $statement = $this->prepare($sql);
            }

            if (!$statement->execute($data)) {
                return false;
            }
        } catch (PDOException $exception) {
            $dbException = new DbException();
            $dbException->setException($exception);
            $dbException->setSql($sql);
            $dbException->setBindData($data);
            throw $dbException;
        }
        return $statement;
    }

    /**
     * @return string
     */
    public function lastInsertId(): string
    {
        return $this->pdo()->lastInsertId();
    }
}